Skip to content

[No QA] Fix stale artifacts cache on iOS - #99185

Merged
roryabraham merged 4 commits into
Expensify:mainfrom
software-mansion-labs:fix-stale-artifacts-cache-on-ios
Sep 4, 2026
Merged

[No QA] Fix stale artifacts cache on iOS#99185
roryabraham merged 4 commits into
Expensify:mainfrom
software-mansion-labs:fix-stale-artifacts-cache-on-ios

Conversation

@mateuuszzzzz

@mateuuszzzzz mateuuszzzzz commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

CocoaPods caches our prebuilt React Native artifacts by their source URL. That URL carries only the
plain react-native version, so a new patches version lands at the same path and CocoaPods keeps the
old extraction. It may not even re-read the podspec that downloads the tarballs. The app then
silently builds against a stale artifact.

Upstream RN has the same issue with Debug and Release artifacts (the cache does not distinguish
them either) and solves it outside CocoaPods. Its [RNCore] build phase re-extracts the framework
whenever the .last_build_configuration marker does not match the current configuration. This PR
extends that pattern to the patches version.

During pod install, when the resolved version does not match the .artifacts-version stamp of
the tarballs in Pods, we drop CocoaPods' memoized podspec with sandbox.remove_local_podspec. This
forces a re-evaluation of the podspec, which re-runs podspec evaluation and the dSYM merge. The re-read
podspec is byte-identical, so Podfile.lock is unaffected.

During the build, a small prelude prepended into the [RNCore] phase compares the tarballs' stamp
with the extracted framework's stamp. On mismatch it writes stale into
.last_build_configuration, and RN's own script re-extracts from the fresh tarballs right after.
This reuses the upstream extraction machinery instead of duplicating it. It lives inside that phase
because CocoaPods sorts phases by name on save, so a separate phase could not run first.

The change covers patches version changes, the RCT_SYMBOLICATE_PREBUILT_FRAMEWORKS toggle, warm
sandboxes, removed Pods and interrupted builds. Debug and Release switching is untouched. When
nothing changed, both pieces are no-ops costing a couple of file reads.


Why this approach instead of a solution where we make CocoaPods differentiate between our patched versions? That would require significantly more effort and more patches to React Native internals. I’d prefer to keep our patched artifacts as close to upstream as possible to minimize the maintenance cost.

Fixed Issues

$ #99187
PROPOSAL:

MOBILE-EXPENSIFY: https://github.com/Expensify/Mobile-Expensify/pull/14066

Tests

Prerequisites: HybridApp iOS dev setup, npm i done, gh auth login (Maven access), with successful pod install + build on this branch with the latest artifacts version (latest main on both repos should be set).

1. Note the current artifact version

  • Run cat Mobile-Expensify/ios/Pods/ReactNativeCore-artifacts/.artifacts-version and note the version (e.g. 0.86.0-5) — referred to as <current> below

2. Force an older artifact version

  • In scripts/artifacts-utils/lib/artifactsResolver.ts, add as the first line of findMatchingArtifactsVersion:
    return '0.86.0-0'; // TEST ONLY — remove after

3. Run pod install and verify it invalidates

  • Run cd Mobile-Expensify/ios && bundle exec pod install
  • Verify the output contains [PatchedArtifacts] Using patched react-native artifacts: react-hybrid:0.86.0-0
  • Verify the output contains [PatchedArtifacts] Artifacts changed to 0.86.0-0; the React-Core-prebuilt podspec will be re-evaluated.
  • Verify cat Pods/ReactNativeCore-artifacts/.artifacts-version prints 0.86.0-0
  • Verify git -C .. diff iOS/Podfile.lock shows no new changes

4. Build and verify the extraction log

  • Build the app via Xcode to see detailed logs
  • In the build log, verify the line: [PatchedArtifacts] Extracted prebuilt React Core is '<current>', tarballs are '0.86.0-0' — marking for re-extraction.
  • Verify it is immediately followed by react-native's Extracting the tarball to temp dir …
  • Verify the build succeeds
  • Verify cat Pods/React-Core-prebuilt/.patched-version prints 0.86.0-0

5. Verify the no-op fast path

  • Build again without any changes
  • Verify the marking for re-extraction line does NOT appear (silent = versions match)
  • Verify there is no tarball extraction for React-Core-prebuilt

6. Revert and verify the round trip

  • Remove the hardcoded line from artifactsResolver.ts
  • Run cd Mobile-Expensify/ios && bundle exec pod install and verify the output contains Artifacts changed to <current> …
  • Build once more and verify the log line:
    … is '0.86.0-0', tarballs are '<current>' — marking for re-extraction. followed by the extraction
  • Verify cat Pods/React-Core-prebuilt/.patched-version is back to <current>
  • Verify app on works without crashing

Byte-level checks

  • Run dwarfdump --uuid Mobile-Expensify/ios/Pods/React-Core-prebuilt/React.xcframework/ios-arm64_x86_64-simulator/React.framework/React
  • Run dwarfdump --uuid <DerivedData>/Build/Products/Debug-iphonesimulator/Expensify.app/Frameworks/React.framework/React
  • Verify the arm64 UUIDs are equal and match the resolved version (0.86.0-0: 6F445E91-…, 0.86.0-5: 7AFB6212-…)

Note: versions -3/-4/-5 ship an identical iOS debug binary, so the optional UUID checks only discriminate with a genuinely different version like 0.86.0-0. Steps 3–6 (logs + stamps) work with any older version.
Note2: possibly on build using old artifact the app will crash due to wrong ABI. On the latest artifact version the app should work as expected.

Offline tests

QA Steps

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

@mateuuszzzzz mateuuszzzzz changed the title Fix stale artifacts cache on iOS [No QA] Fix stale artifacts cache on iOS Aug 21, 2026
Comment thread scripts/artifacts-utils/ios/patched_ios_artifacts.rb Outdated
Comment thread scripts/artifacts-utils/ios/patched_ios_artifacts.rb Outdated
@jakubstec

Copy link
Copy Markdown
Member

looks good, two minor questions 😄

@mateuuszzzzz
mateuuszzzzz marked this pull request as ready for review August 25, 2026 12:37
@mateuuszzzzz
mateuuszzzzz requested a review from a team as a code owner August 25, 2026 12:37
@melvin-bot
melvin-bot Bot requested a review from eh2077 August 25, 2026 12:37
@melvin-bot

melvin-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown

@eh2077 Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot
melvin-bot Bot removed the request for review from a team August 25, 2026 12:37

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a3cb63934e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread scripts/artifacts-utils/ios/patched_ios_artifacts.rb
@roryabraham

Copy link
Copy Markdown
Contributor

@MelvinBot review

@roryabraham

Copy link
Copy Markdown
Contributor

@jakubstec perhaps you can get a peer review from someone at SWM? Also I'm going OOO and don't have time to review this one this week. It seems reasonable, but frankly Ruby is pretty foreign to me

@MelvinBot

Copy link
Copy Markdown
Contributor

Review

The approach is sound and the mechanism holds up. I traced the build-time half against RN's own replace-rncore-version.js: writing stale into .last_build_configuration is a value that matches neither Debug nor Release, so shouldReplaceRnCoreConfiguration returns true and re-extracts, and RN writes the real config back only on success — so a failed/interrupted build correctly retries. The comments are excellent. A few points, none blocking:

1. Confirm the wiring lands in the HybridApp Podfile

This module is only consumed by the HybridApp build, so nothing in this repo calls the new add_sync_prebuilt_script_phase — the post_install hook that invokes it lives in the linked Mobile-Expensify PR. Worth a reviewer double-checking there that add_sync_prebuilt_script_phase(installer) is actually called, since if it isn't, the build-time re-extraction silently never runs and we're back to the original stale-cache bug. The raise guard on L83-L84 only fires if the method runs at all.

2. Agree with the bot: include the package name in the stamp

The stamp at L53-L54 and the written stamp at L266-L267 key only on version, while the prefetch cache and prune_cache already key on package + version precisely because react-hybrid and react-standalone version independently and aren't interchangeable. In a warm sandbox switched between packages where both resolve to the same version string, both the podspec re-eval and the build-time sync would false-negative and keep the wrong artifact. It's an unlikely combo (the two packages normally build from different project_pods_roots), so low severity — but folding resolution['packageName'] into the stamp is cheap belt-and-suspenders and makes the stamp match the cache's own identity model.

3. On the two open questions from the earlier review

  • "Should we explicitly return an exit code?" — This looks already handled: the prepended prelude is bash "…/sync-prebuilt-rncore.sh" || exit 1 (L86) and the script itself runs set -euo pipefail, so a failing prelude does fail the build rather than silently reproducing the stale cache.
  • "Should we raise instead of log here?" — I'd keep the log in force_rncore_podspec_reevaluation. Unlike assert_local_rncore_source, that line is the normal "artifacts changed, re-evaluating" info path, not a detection that our injection failed to land — the actual injection-failure guard already exists as the raise in add_sync_prebuilt_script_phase. Converting it to a raise wouldn't add a real safety net.
Other things I verified (all good)
  • Failed-extraction retry: the sync script writes .patched-version to the new value before RN extracts, but re-extraction is gated on .last_build_configuration, not .patched-version, so a build that dies mid-extraction still re-extracts next time (marker stays stale).
  • set -e short-circuits: [ -f … ] || exit 0 and [ "$TARBALLS" = "$EXTRACTED" ] && exit 0 behave as intended — the test isn't the last command in the AND/OR list, so its "failure" doesn't trip set -e.
  • Idempotency: the unless phase.shell_script.start_with?(prelude) guard prevents a double-prepend across repeated pod installs.
  • +dsym in the stamp correctly makes an RCT_SYMBOLICATE_PREBUILT_FRAMEWORKS toggle count as a change.

I can't exercise the iOS pod install/Xcode flow here, so this is a static review only — the manual test plan in the description is the right way to confirm end-to-end.


view run

@roryabraham
roryabraham self-requested a review September 1, 2026 00:22
@mateuuszzzzz

Copy link
Copy Markdown
Contributor Author

@MelvinBot according to

  1. Agree with the bot: include the package name in the stamp

I don’t think it makes sense to complicate our pipeline here. This implementation is tailored to our specific needs, and it’s highly unlikely that we’ll use it this way in the future.

@MelvinBot

Copy link
Copy Markdown
Contributor

You're right — I checked, and the collision I described can't happen. Dropping the suggestion.

The stamp lives at Pods/ReactNativeCore-artifacts/.artifacts-version, which is under project_pods_root — so it's per-Podfile, not shared. And the two packages never share a Podfile: HybridApp runs pod install in Mobile-Expensify/ios, while standalone NewDot runs in ios/ with RCT_USE_RN_DEP=0 RCT_USE_PREBUILT_RNCORE=0 and an ios/Podfile that doesn't require this module at all. So a given sandbox only ever sees one package name, and the version alone does identify the artifact there. My scenario needed one sandbox to see both — it can't.

Only optional nit left, take it or leave it: the cache comment right below explicitly says "a version alone does not identify an artifact," which is what sent me down this path. A half-line on the stamp noting it's per-sandbox so the package name is fixed would stop the next reader (or bot) from filing the same comment. Zero pipeline complexity.

The other two points from my review stand as-is — nothing blocking from me.


view run

@eh2077

eh2077 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Hi @roryabraham, I can't provide helpful review here as I'm not familiar with Ruby. Can you assign someone else please? thanks!

@mateuuszzzzz

Copy link
Copy Markdown
Contributor Author

Yeah, I think we don't have many ruby developers on board 😅

@mateuuszzzzz

Copy link
Copy Markdown
Contributor Author

Also, I missed this before:

Confirm the wiring lands in the HybridApp Podfile

But I can confirm that wiring lands in the HybridApp Podfile

@roryabraham roryabraham left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍🏼

Only thought is it's not clear to me why you shelled out to a bash script rather than just staying in Ruby, but NAB

@roryabraham

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick).
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Android: mWeb Chrome
iOS: HybridApp
iOS: mWeb Safari
MacOS: Chrome / Safari

@roryabraham
roryabraham merged commit 4591580 into Expensify:main Sep 4, 2026
22 of 29 checks passed
@OSBotify

OSBotify commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants